/*
 * Copyright (c) 2014, 2017 Cisco Systems, Inc. and others.  All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
 * and is available at http://www.eclipse.org/legal/epl-v10.html
 */

package org.opendaylight.controller.config.spi;

import org.opendaylight.controller.config.api.DependencyResolver;
import org.opendaylight.controller.config.api.ModuleIdentifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Base implementation of Module. This implementation contains base logic for
 * Module reconfiguration with associated fields.
 *
 * @param <M>
 *            Type of module implementation. Enables easier implementation for
 *            the <code>isSame()</code> method
 */
public abstract class AbstractModule<M extends AbstractModule<M>>
        implements org.opendaylight.controller.config.spi.Module {

    private static final Logger LOG = LoggerFactory.getLogger(AbstractModule.class);

    protected final DependencyResolver dependencyResolver;
    protected final ModuleIdentifier identifier;

    private AutoCloseable oldInstance;
    private M oldModule;
    private AutoCloseable instance;
    private boolean canReuseInstance = true;

    /**
     * Called when module is configured.
     *
     * @param identifier
     *            id of current instance.
     * @param dependencyResolver
     *            resolver used in dependency injection and validation.
     */
    public AbstractModule(final ModuleIdentifier identifier, final DependencyResolver dependencyResolver) {
        this(identifier, dependencyResolver, null, null);
    }

    /**
     * Called when module is reconfigured.
     *
     * @param identifier
     *            id of current instance.
     * @param dependencyResolver
     *            resolver used in dependency injection and validation.
     * @param oldModule
     *            old instance of module that is being reconfigred(replaced) by
     *            current instance. The old instance can be examined for reuse.
     * @param oldInstance
     *            old instance wrapped by the old module. This is the resource that
     *            is actually being reused if possible or closed otherwise.
     */
    public AbstractModule(final ModuleIdentifier identifier, final DependencyResolver dependencyResolver,
            final M oldModule, final AutoCloseable oldInstance) {
        this.identifier = identifier;
        this.dependencyResolver = dependencyResolver;
        this.oldModule = oldModule;
        this.oldInstance = oldInstance;
    }

    @Override
    public ModuleIdentifier getIdentifier() {
        return identifier;
    }

    public final void setCanReuseInstance(final boolean canReuseInstance) {
        this.canReuseInstance = canReuseInstance;
    }

    /**
     * General algorithm for spawning/closing and reusing wrapped instances.
     *
     * @return current instance of wrapped resource either by reusing the old one
     *         (if present) or constructing a brand new.
     */
    @Override
    @SuppressWarnings("IllegalCatch")
    public final AutoCloseable getInstance() {
        if (instance == null) {
            if (oldInstance != null && canReuseInstance && canReuseInstance(oldModule)) {
                resolveDependencies();
                instance = reuseInstance(oldInstance);
            } else {
                if (oldInstance != null) {
                    try {
                        oldInstance.close();
                    } catch (final Exception exception) {
                        LOG.error("An error occurred while closing old instance {} for module {}", oldInstance,
                                getIdentifier(), exception);
                    }
                }
                resolveDependencies();
                instance = createInstance();
                if (instance == null) {
                    throw new IllegalStateException(
                            "Error in createInstance - null is not allowed as return value. Module: "
                                    + getIdentifier());
                }
            }

            // Prevent serial memory leak: clear these references as we will not use them
            // again.
            oldInstance = null;
            oldModule = null;
        }

        return instance;
    }

    /**
     * Create instance.
     *
     * @return Brand new instance of wrapped class in case no previous instance is
     *         present or reconfiguration is impossible.
     */
    protected abstract AutoCloseable createInstance();

    @Override
    public final boolean canReuse(final Module prevModule) {
        // Just cast into a specific instance
        // TODO unify this method with canReuseInstance (required Module interface to be
        // generic which requires quite a lot of changes)
        return canReuseInstance && getClass().isInstance(prevModule) ? canReuseInstance((M) prevModule) : false;
    }

    /**
     * Users are welcome to override this method to provide custom logic for
     * advanced reusability detection.
     *
     * @param prevModule
     *            old instance of a Module
     * @return true if the old instance is reusable false if a new one should be
     *         spawned
     */
    protected abstract boolean canReuseInstance(M prevModule);

    /**
     * By default the oldInstance is returned since this method is by default called
     * only if the oldModule had the same configuration and dependencies configured.
     * Users are welcome to override this method to provide custom logic for
     * advanced reusability.
     *
     * @param prevInstance
     *            old instance of a class wrapped by the module
     * @return reused instance
     */
    protected AutoCloseable reuseInstance(final AutoCloseable prevInstance) {
        // implement if instance reuse should be supported. Override canReuseInstance to
        // change the criteria.
        return prevInstance;
    }

    /**
     * Inject all the dependencies using dependency resolver instance.
     */
    protected abstract void resolveDependencies();
}
